package game.square.temperature;

import be.kuleuven.cs.som.annotate.*;

/**
 * A class of temperatures, with methods to return the value of the temperature
 * in the temperature scale of this class and to set the value of the temperature.
 * 
 * @invar 	...  
 * 		  | canHaveAsTemperature(getTemperatureValue())
 * @invar	...
 * 		  | areValidMaxMinTemperatures(getMinTemperature(),getMaxTemperature(),
 * 		  |		Temperature.getTemperatureScale())
 * 
 * @version 1.0
 * @author Chris Desomer, Geert Heyman
 */
public class Temperature {
	
	/**
	 * Initialize this temperature with the given values, expressed in the given temperature scale.
	 * 
	 * @param 	temperature
	 * 			The new value for the temperature of this new temperature.
	 * @param 	minTemperature
	 * 			The new value for the minimum temperature of this new temperature.
	 * @param 	maxTemperature
	 * 			The new value for the maximum temperature of this new temperature.
	 * @param 	temperatureScale
	 * 			The temperature scale in which the given temperatures are expressed.
	 * @post	the minimum temperature is equal to the given minimum temperature
	 *		  |	new.getMinTemperature() == minTemperature
	 * @post	the maximum temperature is equal to the given maximum temperature
	 *		  |	new.getMaxTemperature() == maxTemperature
	 * @effect 	The temperature of this new square is equal to the given
	 *       	temperature. 
	 *        | new.setTemperature(temperature,temperatureScale)
	 * @throws 	IllegalArgumentException
	 * 			The given temperature scale is not effective or the minimum or maximum
	 * 			temperatures are not valid.
	 */
	@Raw
	public Temperature(int temperature, int minTemperature, int maxTemperature,
			TemperatureScale temperatureScale) throws IllegalArgumentException, IllegalStateException {
		if (!areValidMaxMinTemperatures(minTemperature, maxTemperature, temperatureScale))
			throw new IllegalArgumentException();
		this.minTemperature = toTemperatureScale(minTemperature,temperatureScale,TemperatureScale.CELCIUS);
		this.maxTemperature = toTemperatureScale(maxTemperature,temperatureScale,TemperatureScale.CELCIUS);
		setTemperatureValue(temperature, temperatureScale);
	}
	/**
	 * Return the value of this temperature in the temperature scale of this temperature.
	 */
	public int getTemperatureValue(TemperatureScale temperatureScale) {
		return toTemperatureScale(temperature, TemperatureScale.CELCIUS, temperatureScale);
	}
	
	
	/**
	 * Check if the given temperature has the same temperature value
	 * that is physically equal to the temperature value of this square.
	 * 
	 * @param 	temperature
	 * 			The temperature to check.
	 * @return	...
	 * 		  | result == getTemperatureValue(TemperatureScale.CELCIUS) == 
	 *		  |				temperature.getTemperatureValue(TemperatureScale.CELCIUS)
	 */
	public boolean hasEqualTemperatureValueAs(Temperature temperature){
		return getTemperatureValue(TemperatureScale.CELCIUS) == temperature.getTemperatureValue(TemperatureScale.CELCIUS);
	}

	
	/**
	 * Set the temperature of this temperature object to the given temperature expressed in degrees Celcius.
	 * 
	 * @param 	temperature
	 *          The new value for the temperature
	 * @param 	temperatureScale
	 * 			the temperature scale in which the temperature is expressed.
	 * @post  	The temperature of this square is equal to the given temperature
	 * 			expressed in degrees Celcius. 
	 *        | new.getTemperatureValue()== toTemperatureScale(temperature, temperatureScale, TemperatureScale.CELCIUS)
	 * @throws	IllegalStateException
	 * 			The temperature scale of this temperature is not effective.
	 * 		  | getTemperatureScale() == null
	 * @throws 	IllegalArgumentException
	 * 			The given temperature is not a valid temperature
	 * 		  | !canHaveAsTemperature(temperature, temperatureScale, TemperatureScale.CELCIUS) 	
	 */
	public void setTemperatureValue(int temperature, TemperatureScale temperatureScale) 
						throws IllegalStateException, IllegalArgumentException {
		if (temperatureScale == null)
			throw new IllegalStateException("Temperature scale is not effective");
		if (!canHaveAsTemperature(temperature, temperatureScale))
			throw new IllegalArgumentException("The given temperature is not valid.");
		this.temperature = toTemperatureScale(temperature,
				temperatureScale, TemperatureScale.CELCIUS);
	}
	
	/**
	 * Check if the given value is a valid temperature for the temperature scale.
	 * 
	 * @param 	temperature
	 *          The temperature to check
	 * @param	temperatureScale
	 * 			the temperature scale in which the temperature needs to be expressed
	 * @return 	The method returns true if the temperature lies between the
	 *         	minimum and maximum temperature of this temperature object,
	 *          taking into account the temperature scale of this class.    
	 *        | result == (temperature <= getMaxTemperature(temperatureScale)) && 
	 *        | 			(temperature >=getMinTemperature(temperatureScale))
	 */
	@Raw
	public boolean canHaveAsTemperature(int temperature, TemperatureScale temperatureScale) {
		return temperature <= getMaxTemperature(temperatureScale) &&
		 			temperature >= getMinTemperature(temperatureScale);
	}
	
	/**
	 * Variable that stores the value of the temperature in degrees celcius.
	 */
	private int temperature;
	
	/**
	 * Return the maximum temperature of this temperature object in the temperature scale 
	 * this class.
	 * 
	 * @param temperatureScale
	 * 		  The temperature scale in which the maximum temperature is expressed
	 * @note  the method isn't annotated immutable because the return value depends on the
	 * 		  temperature scale.
	 */
	public int getMaxTemperature(TemperatureScale temperatureScale) {
		return toTemperatureScale(maxTemperature, TemperatureScale.CELCIUS, temperatureScale);
	}
	
	
	/**
	 * Check if the given values for the maximum and the minimum temperature are valid
	 * values for the maximum and minimum temperature of this temperature.
	 * 
	 * @param	minTemperature
	 * 			The minimum temperature to check.
	 * @param	maxTemperature
	 * 			The maximum temperature to check.
	 * @param	temperatureScale
	 * 			The temperature scale in which the minimum and maximum temperature
	 * 			need to be expressed
	 * @return	False if the given minimum temperature is greater 
	 * 			than or equal to the given maximum temperature
	 * 		  |	if (minTemperature >= maxTemperature)
	 * 		  |		then result == false
	 * @return	The method returns true if and only if the given minimum and maximum 
	 * 			temperatures are valid and the minimum temperature is less than
	 * 			the given maximum temperature.
	 * 		  | if (minTemperature < maxTemperature)
	 * 		  | result == (isValidMaxTemperature(maxTemperature, temperatureScale) &&
					isValidMinTemperature(minTemperature, temperatureScale))
	 */
	@Raw
	public boolean areValidMaxMinTemperatures(int minTemperature,
			int maxTemperature, TemperatureScale temperatureScale)
			throws IllegalArgumentException {
		if (minTemperature >= maxTemperature)
			return false;
		return (isValidMaxTemperature(maxTemperature, temperatureScale) &&
					isValidMinTemperature(minTemperature, temperatureScale));
	}
	/**
	 * Check if the given maximum temperature is valid. 
	 * 
	 * @param 	maxTemperature
	 * 			The maximum temperature to check.
	 * @param	temperatureScale
	 * 			The temperature scale in which the given maximum temperature is expressed.
	 * @return  The method returns true if the given maximum temperature expressed in fahrenheit
	 * 			is less than or equal to the maximum value an integer can represent.
	 * 		  | result == maxTemperature <= toTemperatureScale(Integer.MAX_VALUE,
	 * 		  |									TemperatureScale.FAHRENHEIT, temperatureScale)
	 * @throws	IllegalArgumentException
	 * 			The given temperature scale is not effective.
	 * 		  |	temperatureScale == null		
	 */
	@Raw
	public static boolean isValidMaxTemperature(int maxTemperature, TemperatureScale temperatureScale) 
					throws IllegalArgumentException {
		if (temperatureScale == null)
			throw new IllegalArgumentException(
					"The given temperature scale is not effective");
		// for high temperatures the representation in fahrenheit will 
		// give the highest integer.
		if (temperatureScale == TemperatureScale.CELCIUS)
			return maxTemperature < (Integer.MAX_VALUE - 32)*5l/9l;
		return maxTemperature < (Integer.MAX_VALUE + 459.67)*5l/9l;
			
	}
	
	/**
	 * Variable registering the maximum temperature of this square in degrees Celcius.
	 */
	private final int maxTemperature;
	
	/**
	 * Return the minimum temperature of this temperature object in the temperature scale 
	 * of this class.
	 * 
	 * @param	temperatureScale
	 * 			the temperature scale in which the minimum temperature is expressed.
	 * @note 	the method isn't annotated immutable, because the return value depends on the
	 * 			temperature scale.	 
	 */
	@Raw
	public int getMinTemperature(TemperatureScale temperatureScale) {
		return toTemperatureScale(minTemperature, TemperatureScale.CELCIUS, temperatureScale);
	}
	
	/**
	 * Check if the given minimum temperature is valid. 
	 * 
	 * @param 	minTemperature
	 * 			The minimum temperature to check.
	 * @param	temperatureScale
	 * 			The temperature scale in which the given minimum temperature
	 * 			is expressed.
	 * @return  The method returns true if the given temperature expressed in
	 * 			kelvin is more than zero.
	 * 		  | result == toTemperatureScale(minTemperature, temperatureScale, TemperatureScale.KELVIN ) > 0
	 * @throws	IllegalArgumentException
	 * 			The given temperature scale is not effective.
	 * 		  | temperatureScale == null		
	 */
	@Raw
	public static boolean isValidMinTemperature(int minTemperature, TemperatureScale temperatureScale) 
					throws IllegalArgumentException {
		if (temperatureScale == null)
			throw new IllegalArgumentException(
					"The given temperature scale is not effective");
		if (temperatureScale == TemperatureScale.CELCIUS)
			return minTemperature+ 273.15 > 0;
		return (minTemperature + 459.67)*5l/9l > 0;
	}
	
	/**
	 * Variable registering the minimum temperature of this square in degrees celcius.
	 */
	private final int minTemperature;
	
	/**
	 * Return the temperature of the given temperature (in a certain current
	 *  temperature scale) in the given temperature target scale.
	 * 
	 * @param 	temperature
	 * 			The temperature that should be converted.
	 * @param 	tempScaleCurrent 
	 * 			The scale in which the given temperature is expressed.
	 * @param 	tempScaleTarget 
	 * 			The temperature scale in which the temperature
	 * 			should be converted to.
	 * @return 	The method returns the given temperature expressed in the
	 * 			appropriate temperature scale. 
	 * 		  | result == Math.round((temperature +
	 * 		  |		tempScaleCurrent.getConvertTerm(tempScaleTarget).floatValue())
	 * 		  |		*tempScaleCurrent.getConvertFactor(tempScaleTarget).floatValue())
	 * @throws 	IllegalArgumentException
	 * 			One of the given temperature scales is not effective.
	 * 		  |	tempScaleCurrent == null || tempScaleTarget == null
	 * @throws	IllegalArgumentException
	 * 			The given temperature can cause overflow or is physically impossible.
	 * 		  | !isValidMinTemperature(temperature, tempScaleCurrent)
	 *		  |		|| !isValidMaxTemperature(temperature, tempScaleCurrent)
	 * 			
	 */
	@Raw
	public static int toTemperatureScale(int temperature,TemperatureScale tempScaleCurrent,
			TemperatureScale tempScaleTarget)
			throws IllegalArgumentException {
		if (tempScaleCurrent == null || tempScaleTarget == null)
			throw new IllegalArgumentException(
					"One of the given temperature scales is not effective");
		if (!isValidMinTemperature(temperature, tempScaleCurrent)
				|| !isValidMaxTemperature(temperature, tempScaleCurrent))
			throw new IllegalArgumentException(
					"The given temperature can cause overflow or is physically impossible");
		float convertTerm = tempScaleCurrent.getConvertTerm(
				tempScaleTarget).floatValue();
		float convertFactor = tempScaleCurrent.getConvertFactor(
				tempScaleTarget).floatValue();
		return Math.round((temperature + convertTerm) * convertFactor);
	}
	/**
	 * Return the weighted average temperature of this temperature and a number
	 * of temperatures equal to the given temperature.
	 * 
	 * @param 	temperature
	 * 			The temperature object to calculate the average weight with.
	 * @param 	weightThisTemperature
	 * 			The weight factor for this temperature.
	 * @param	weightOtherTemperature
	 * 			The weight factor for the other temperature.
	 * @return	A new temperature object which has a temperature value that
	 * 			is the weighted average of this temperature and the given temperature, 
	 * 			weighted with the given weight factors.
	 */
	public Temperature calculateWeightedAverageWith(Temperature temperature, 
			int weightThisTemperature, int weightOtherTemperature) {
		int temperatureValueGivenCel = temperature.getTemperatureValue(TemperatureScale.CELCIUS);
		int temperatureValueThisCel = getTemperatureValue(TemperatureScale.CELCIUS);
		long averageTemperature = (temperatureValueGivenCel *weightOtherTemperature
				+ temperatureValueThisCel*weightThisTemperature) / (weightOtherTemperature+weightThisTemperature);
		return new Temperature(Math.round(averageTemperature),
				getMinTemperature(TemperatureScale.CELCIUS), getMaxTemperature(TemperatureScale.CELCIUS),
				TemperatureScale.CELCIUS);
	}
	
	/**
	 * Return a new temperature that is the sum of this temperature
	 * and the given temperature, taking in account the temperature
	 * scales.
	 * 
	 * @param 	other
	 * 			The other temperature to add to this temperature
	 * @pre		..
	 * 		  | other != null
	 * @return	..
	 * 		  | result.getTemperatureValue(temperatureScale) == this.getTemperatureValue(temperatureScale))
	 * 		  |	 	+ temperature.getTemperatureValue(TemperatureScale.CELCIUS)) &&
	 * @result	..
	 * 		  | result.getMinTemperature(TemperatureScale.CELCIUS) == Math.max(getMinTemperature(TemperatureScale.CELCIUS), 
	 * 		  |						other.getMinTemperature(TemperatureScale.CELCIUS)
	 * @result	..
	 * 		  | result.getMaxTemperature(TemperatureScale.CELCIUS) == Math.min(getMaxTemperature(TemperatureScale.CELCIUS), 
	 * 		  |						other.getMaxTemperature(TemperatureScale.CELCIUS)
	 */
	public Temperature add(Temperature other) throws IllegalArgumentException{
		assert (other !=null);
		int thisTemp =getTemperatureValue(TemperatureScale.CELCIUS);
		int otherTemp = other.getTemperatureValue(TemperatureScale.CELCIUS);
		int thisMin = getMinTemperature(TemperatureScale.CELCIUS);
		int otherMin = other.getMinTemperature(TemperatureScale.CELCIUS);
		int thisMax = getMaxTemperature(TemperatureScale.CELCIUS);
		int otherMax = other.getMaxTemperature(TemperatureScale.CELCIUS);
		int minTemp = Math.max(thisMin, otherMin);
		int maxTemp = Math.min(thisMax, otherMax);
		return new Temperature(thisTemp+otherTemp, minTemp, maxTemp, TemperatureScale.CELCIUS);

	}
	
}
